package com.easychat.websocket;

import com.alibaba.fastjson.JSONObject;
import com.easychat.constants.Constants;
import com.easychat.entity.dto.MessageSendDTO;
import com.easychat.entity.dto.WsInitData;
import com.easychat.entity.enums.MessageTypeEnum;
import com.easychat.entity.enums.UserContactApplyStatusEnum;
import com.easychat.entity.enums.UserTypeEnum;
import com.easychat.entity.po.ChatMessage;
import com.easychat.entity.po.ChatSessionUser;
import com.easychat.entity.po.UserContactApply;
import com.easychat.entity.po.UserInfo;
import com.easychat.entity.query.ChatMessageQuery;
import com.easychat.entity.query.ChatSessionUserQuery;
import com.easychat.entity.query.UserContactApplyQuery;
import com.easychat.entity.query.UserInfoQuery;
import com.easychat.mappers.ChatMessageMapper;
import com.easychat.mappers.ChatSessionUserMapper;
import com.easychat.mappers.UserContactApplyMapper;
import com.easychat.mappers.UserInfoMapper;
import com.easychat.redis.RedisComponent;
import com.easychat.service.ChatSessionUserService;
import com.easychat.utils.StringTools;
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.AttributeMap;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class ChannelContextUtils {

    private static final ConcurrentHashMap<String, Channel> USER_CONTEXT_MAP = new ConcurrentHashMap<>();
    private static final ConcurrentHashMap<String, ChannelGroup> GROUP_CONTEXT_MAP = new ConcurrentHashMap<>();
    @Resource
    private RedisComponent redisComponent;
    @Resource
    private UserInfoMapper<UserInfo, UserInfoQuery> userInfoMapper;
    @Resource
    private ChatSessionUserMapper<ChatSessionUser, ChatSessionUserQuery> chatSessionUserMapper;
    @Resource
    private ChatMessageMapper<ChatMessage, ChatMessageQuery> chatMessageMapper;
    @Resource
    private UserContactApplyMapper<UserContactApply, UserContactApplyQuery> userContactApplyMapper;
    @Resource
    private ChatSessionUserService chatSessionUserService;

    public void addContext(String userId, Channel channel) {
        String channelId = channel.id().toString();//每个channel的唯一id
        //Type parameters:
        //<T> – the type of the Attribute which can be accessed via this AttributeKey.
        AttributeKey<String> attributeKey = null;
        if (!AttributeKey.exists(channelId)) {//Returns true if a AttributeKey exists for the given name.
            //Creates a new AttributeKey for the given name or
            // fail with an IllegalArgumentException if a
            // AttributeKey for the given name exists
            //如果不存在name为channelId的AttributeKey，就创建一个新的
            attributeKey = AttributeKey.newInstance(channelId);
        } else {
            //Returns the singleton instance of the AttributeKey
            // which has the specified name
            //如果存在name为channelId的AttributeKey，就返回已有的
            attributeKey = AttributeKey.valueOf(channelId);
        }
        //Get the Attribute for the given AttributeKey.
        //Type parameters:
        //<T> – the type of the value it holds.
        //通过attributeKey获取对应的attribute
        Attribute<String> attribute = channel.attr(attributeKey);
        //Sets the value
        //channel<---->AttributeMap<---->{ attributeKey(name = channelId) + attribute(value = userId) }
        attribute.set(userId);
        //获取userId对应的联系人id列表
        List<String> contactIdList = redisComponent.getUserContact(userId);
        for (String contactId : contactIdList) {
            //如果contactId开头是G，说明为群组id
            if (contactId.startsWith(UserTypeEnum.GROUP.getPrefix())) {
                //将channel添加到对应的群中，一个channel(userId)可能属于多个群组
                add2Group(contactId, channel);
            }
        }
        //将userId和对应的channel管理起来
        USER_CONTEXT_MAP.put(userId, channel);
        //在建立连接时保存心跳
        redisComponent.saveUserHeartBeat(userId);
        //将userId和对应的端口号保存起来，方便消息推送时正确路由，找到联系人所在服务器对应的（mq）channel
        redisComponent.saveServerPort(userId);

        //更新用户最后连接时间
        UserInfo userInfo = new UserInfo();
        userInfo.setLastLoginTime(new Date());
        userInfoMapper.updateByUserId(userInfo, userId);

        //给用户发消息
        //查历史消息时，根据用户最后离线时间到现在的间隔，最多查3天前
        //若用户最后离线时间为7天前，查3天
        //若用户最后离线时间为2天前，查2天
        UserInfo dbInfo = userInfoMapper.selectByUserId(userId);
        Long lastOffTime = dbInfo.getLastOffTime();
        Long earliestTime = null;
        Long curTime = System.currentTimeMillis();
        if (lastOffTime != null && curTime - Constants.MILLIS_3DAYS > lastOffTime) {
            //若用户最后离线时间为7天前，查3天
            earliestTime = curTime - Constants.MILLIS_3DAYS;
        } else if (lastOffTime != null && curTime - Constants.MILLIS_3DAYS <= lastOffTime) {
            //若用户最后离线时间为2天前，查2天
            earliestTime = lastOffTime;
        }

        //1. 查询当前用户所有的会话信息，保证换了设备信息也同步
        ChatSessionUserQuery sessionUserQuery = new ChatSessionUserQuery();
        sessionUserQuery.setUserId(userId);
        sessionUserQuery.setOrderBy("last_receive_time desc");
        List<ChatSessionUser> chatSessionList = chatSessionUserService.findListByParam(sessionUserQuery);
        //ws初始化时发给客户端的数据
        WsInitData wsInitData = new WsInitData();
        wsInitData.setChatSessionList(chatSessionList);

        //2. 查询聊天消息
        //查询chat_message表中contact_id(接收人)为该用户的消息，以及contact_id(接收人)为该用户所在群组的消息
        ChatMessageQuery chatMessageQuery = new ChatMessageQuery();
        List<String> contactUserOrGroupIdList = new ArrayList<>();
        contactUserOrGroupIdList.add(userId);//contact_id(接收人)为该用户
        for (String contactId : contactIdList) {//contactIdList：该用户所有联系人id
            if (contactId.startsWith(UserTypeEnum.GROUP.getPrefix())) {//该用户所在群组id
                contactUserOrGroupIdList.add(contactId);
            }
        }
        chatMessageQuery.setContactIdList(contactUserOrGroupIdList);
        chatMessageQuery.setLastReceiveTime(earliestTime);//设置最早能查询多早的历史消息
        List<ChatMessage> chatMessageList = chatMessageMapper.selectList(chatMessageQuery);
        wsInitData.setChatMessageList(chatMessageList);

        //3. 查询好友申请
        UserContactApplyQuery userContactApplyQuery = new UserContactApplyQuery();
        userContactApplyQuery.setReceiveUserId(userId);//接收人为当前用户
        userContactApplyQuery.setStatus(UserContactApplyStatusEnum.PENDING.getStatus());
        userContactApplyQuery.setLastApplyTimestamp(earliestTime);//设置最早能查询多早的历史申请
        Integer applyCount = userContactApplyMapper.selectCount(userContactApplyQuery);
        wsInitData.setApplyCount(applyCount);
        //发送消息给当前登录用户
        MessageSendDTO<WsInitData> messageSendDTO = new MessageSendDTO<>();
        messageSendDTO.setContactId(userId);//这里contactId就是userId，因为要发给当前登录的用户
        messageSendDTO.setMessageType(MessageTypeEnum.INIT.getType());
        messageSendDTO.setExtendData(wsInitData);
        sendMessage(messageSendDTO, userId);

        //演示内容
//        String groupId = "888";
//        //GROUP_CONTEXT_MAP中某个groupId对应一个channelGroup
//        // channelGroup中可以添加多个channel
//        //可以统一向这些channel发送消息
//        add2Group(groupId, channel);
    }

    /**
     * 将channel添加到GROUP_CONTEXT_MAP中groupId对应的channelGroup里
     * 不同的groupId对应的channelGroup可看成不同的群组，同群组的channel可实现群发消息
     * @param groupId
     * @param channel
     */
    public void add2Group(String groupId, Channel channel) {
        //public interface ChannelGroup extends Set<Channel>, Comparable<ChannelGroup>
        //ChannelGroup实际上是Channel的集合
        ChannelGroup channelGroup = GROUP_CONTEXT_MAP.get(groupId);//获取groupId对应的channelGroup
        if (channelGroup == null) {
            //创建channelGroup
            channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
            //将groupId-channelGroup存入map
            GROUP_CONTEXT_MAP.put(groupId, channelGroup);
        }
        if (channel == null) {
            return;
        }
        //将channel添加到channelGroup
        //可以统一向这些channel中发送消息
        channelGroup.add(channel);
    }

    /**
     * 将userId的用户添加到id为groupId的群组
     * @param userId
     * @param groupId
     */
    public void addUser2Group(String userId, String groupId) {
        Channel channel = USER_CONTEXT_MAP.get(userId);
        add2Group(groupId, channel);
    }

    /**
     * 发送消息给用户或群组
     * @param messageSendDTO
     */
    public void sendMessageToUserOrGroup(MessageSendDTO messageSendDTO) {
        UserTypeEnum userTypeEnum = UserTypeEnum.getByPrefix(messageSendDTO.getContactId());
        switch (userTypeEnum) {
            case USER:
                send2User(messageSendDTO);
                break;
            case GROUP:
                send2Group(messageSendDTO);
                break;
        }
    }

    //发消息给用户
    private void send2User(MessageSendDTO messageSendDTO) {
        String contactId = messageSendDTO.getContactId();
        if (StringTools.isEmpty(contactId)) {
            return;
        }
        sendMessage(messageSendDTO, contactId);
        //强制下线
        if (MessageTypeEnum.FORCE_OFF_LINE.getType().equals(messageSendDTO.getMessageType())) {
            closeContext(contactId);
        }
    }

    //强制下线，关闭通道
    public void closeContext(String userId) {
        if (StringTools.isEmpty(userId)) {
            return;
        }
        redisComponent.cleanUserTokenByUserId(userId);//清除redis中对应的token信息
        Channel channel = USER_CONTEXT_MAP.get(userId);
        if (channel == null) {
            return;
        }
        channel.close();//关闭通道
    }

    //发信息给群组
    private void send2Group(MessageSendDTO messageSendDTO) {
        String groupId = messageSendDTO.getContactId();
        if (StringTools.isEmpty(groupId)) {
            return;
        }
        ChannelGroup channelGroup = GROUP_CONTEXT_MAP.get(groupId);//获取groupId对应的channelGroup
        if (channelGroup == null) {
            return;
        }
        channelGroup.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageSendDTO)));

        //离开群聊
        MessageTypeEnum messageTypeEnum = MessageTypeEnum.getByType(messageSendDTO.getMessageType());
        if (messageTypeEnum == MessageTypeEnum.LEAVE_GROUP || messageTypeEnum == MessageTypeEnum.REMOVE_GROUP) {
            String userId = (String) messageSendDTO.getExtendData();//离开群聊的人
            redisComponent.removeUserContact(userId, groupId);//移除联系人  //TODO 离开群聊功能有问题，这里没有成功移除联系人，测试1111的联系人列表(redis)里仍然有测试群
            //离开群聊了，移除这个人的channel
            Channel channel = USER_CONTEXT_MAP.get(userId);
            if (channel == null) {
                return;
            }
            channelGroup.remove(channel);
        }
        //解散群组
        if (messageTypeEnum == MessageTypeEnum.DISSOLUTION_GROUP) {
            GROUP_CONTEXT_MAP.remove(groupId);
            channelGroup.close();
        }
    }

    /**
     * 发送消息给客户端
     * @param messageSendDTO 待发送的消息
     * @param receiveId 发给哪个用户
     */
    public void sendMessage(MessageSendDTO messageSendDTO, String receiveId) {
        if (receiveId == null) {
            return;
        }
        //获取receiveId对应的channel
        Channel channel = USER_CONTEXT_MAP.get(receiveId);
        if (channel == null) {
            return;
        }
        if (MessageTypeEnum.ADD_FRIEND_SELF.getType().equals(messageSendDTO.getMessageType())) {
            /* addContact方法中，发给申请人的消息里，contactId为applyUserId，
            表示消息发送的方向为applyUserId，即sendMessage方法的receiveId就是applyUserId，
            在这里messageSendDTO中的contactId要改成相对于applyUserId客户端的联系人id，即messageSendDTO
            在发给客户端后，所谓的联系人是相对客户端而言的。
            //发消息给申请人，此时发送人和联系人反过来
            messageSendDTO.setMessageType(MessageTypeEnum.ADD_FRIEND_SELF.getType());
            messageSendDTO.setContactId(applyUserId);
            messageSendDTO.setExtendData(contactUserInfo);
            messageHandler.sendMessage(messageSendDTO);*/
            UserInfo contactUserInfo = (UserInfo) messageSendDTO.getExtendData();
            messageSendDTO.setContactId(contactUserInfo.getUserId());
            messageSendDTO.setContactName(contactUserInfo.getNickName());
            messageSendDTO.setExtendData(null);
            messageSendDTO.setMessageType(MessageTypeEnum.ADD_FRIEND.getType());
        } else {
            //相对于客户端(对应服务端的发送方向receiveId)而言，联系人是发送者(sendUserId)
            messageSendDTO.setContactId(messageSendDTO.getSendUserId());
            messageSendDTO.setContactName(messageSendDTO.getSendUserNickName());
        }
        //发送消息给receiveId对应的channel
        channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageSendDTO)));
    }


    //演示方法
//    /**
//     * 将message发送给群组
//     * @param message
//     */
//    public void send2Group(String message) {
//        ChannelGroup channelGroup = GROUP_CONTEXT_MAP.get("888");
//        channelGroup.writeAndFlush(new TextWebSocketFrame(message));
//        //不良的实现方式：遍历所有channel，一个一个的发信息，实现群发
////        USER_CONTEXT_MAP.forEach((k,v)->{
////
////        });
//    }
    public void removeContext(Channel channel) {
        Attribute<String> attribute = channel.attr(AttributeKey.valueOf(channel.id().toString()));
        String userId = attribute.get();
        //TODO 检查逻辑
        if (!StringTools.isEmpty(userId)) {
            USER_CONTEXT_MAP.remove(userId);
            //清除心跳
            redisComponent.cleanUserHeartBeat(userId);
            //清除用户对应的服务器信息
            redisComponent.removeServerPort(userId);
            //更新用户最后离线时间
            UserInfo userInfo = new UserInfo();
            userInfo.setLastOffTime(System.currentTimeMillis());
            userInfoMapper.updateByUserId(userInfo, userId);
        }

    }

}
